/*-------------------------------------------------------------------------
 *
 * nbtree.c
 *	  Postgres 的 Lehman 和 Yao 的 btree 管理算法的实现。
 *
 * NOTES
 *	  此文件仅包含公共接口例程。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/access/nbtree/nbtree.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/nbtree.h"
#include "access/nbtxlog.h"
#include "access/relscan.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "commands/progress.h"
#include "commands/vacuum.h"
#include "miscadmin.h"
#include "nodes/execnodes.h"
#include "pgstat.h"
#include "postmaster/autovacuum.h"
#include "storage/condition_variable.h"
#include "storage/indexfsm.h"
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/smgr.h"
#include "utils/builtins.h"
#include "utils/index_selfuncs.h"
#include "utils/memutils.h"


/*
 * BTPARALLEL_NOT_INITIALIZED 表示扫描尚未开始。
 *
 * BTPARALLEL_ADVANCING 表示某个进程正在将扫描推进到
 * 新页面；其他进程必须等待。
 *
 * BTPARALLEL_IDLE 表示当前没有后端正在将扫描推进到
 * 新页面；某个进程可以开始这样做。
 *
 * BTPARALLEL_DONE 表示扫描已完成（包括错误退出）。
 * 对于每个不同的数组键组合，我们仅达到一次此状态。
 */
typedef enum
{
	BTPARALLEL_NOT_INITIALIZED,
	BTPARALLEL_ADVANCING,
	BTPARALLEL_IDLE,
	BTPARALLEL_DONE
} BTPS_State;

/*
 * BTParallelScanDescData 包含进行并行扫描所需的
 * B树特定共享信息。
 */
typedef struct BTParallelScanDescData
{
	BlockNumber btps_scanPage;	/* 最新或下一个要扫描的页面 */
	BTPS_State	btps_pageStatus;	/* 指示下一个页面是否
									 * 可用于扫描。详情请参见上面
									 * 并行扫描的可能状态。 */
	int			btps_arrayKeyCount; /* 表示并行扫描处理的数组扫描
									 * 键的数量 */
	slock_t		btps_mutex;		/* 保护上述变量 */
	ConditionVariable btps_cv;	/* 用于同步并行扫描 */
}			BTParallelScanDescData;

typedef struct BTParallelScanDescData *BTParallelScanDesc;


static void fc_btvacuumscan(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats,
						 IndexBulkDeleteCallback fc_callback, void *fc_callback_state,
						 BTCycleId fc_cycleid);
static void fc_btvacuumpage(BTVacState *fc_vstate, BlockNumber fc_scanblkno);
static BTVacuumPosting fc_btreevacuumposting(BTVacState *fc_vstate,
										  IndexTuple fc_posting,
										  OffsetNumber fc_updatedoffset,
										  int *fc_nremaining);


/*
 * B树处理程序函数：返回带有访问方法参数
 * 和回调的 IndexAmRoutine。
 */
Datum bthandler(PG_FUNCTION_ARGS)
{
	IndexAmRoutine *fc_amroutine = makeNode(IndexAmRoutine);

	fc_amroutine->amstrategies = BTMaxStrategyNumber;
	fc_amroutine->amsupport = BTNProcs;
	fc_amroutine->amoptsprocnum = BTOPTIONS_PROC;
	fc_amroutine->amcanorder = true;
	fc_amroutine->amcanorderbyop = false;
	fc_amroutine->amcanbackward = true;
	fc_amroutine->amcanunique = true;
	fc_amroutine->amcanmulticol = true;
	fc_amroutine->amoptionalkey = true;
	fc_amroutine->amsearcharray = true;
	fc_amroutine->amsearchnulls = true;
	fc_amroutine->amstorage = false;
	fc_amroutine->amclusterable = true;
	fc_amroutine->ampredlocks = true;
	fc_amroutine->amcanparallel = true;
	fc_amroutine->amcaninclude = true;
	fc_amroutine->amusemaintenanceworkmem = false;
	fc_amroutine->amparallelvacuumoptions =
		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
	fc_amroutine->amkeytype = InvalidOid;

	fc_amroutine->ambuild = btbuild;
	fc_amroutine->ambuildempty = btbuildempty;
	fc_amroutine->aminsert = btinsert;
	fc_amroutine->ambulkdelete = btbulkdelete;
	fc_amroutine->amvacuumcleanup = btvacuumcleanup;
	fc_amroutine->amcanreturn = btcanreturn;
	fc_amroutine->amcostestimate = btcostestimate;
	fc_amroutine->amoptions = btoptions;
	fc_amroutine->amproperty = btproperty;
	fc_amroutine->ambuildphasename = btbuildphasename;
	fc_amroutine->amvalidate = btvalidate;
	fc_amroutine->amadjustmembers = btadjustmembers;
	fc_amroutine->ambeginscan = btbeginscan;
	fc_amroutine->amrescan = btrescan;
	fc_amroutine->amgettuple = btgettuple;
	fc_amroutine->amgetbitmap = btgetbitmap;
	fc_amroutine->amendscan = btendscan;
	fc_amroutine->ammarkpos = btmarkpos;
	fc_amroutine->amrestrpos = btrestrpos;
	fc_amroutine->amestimateparallelscan = btestimateparallelscan;
	fc_amroutine->aminitparallelscan = btinitparallelscan;
	fc_amroutine->amparallelrescan = btparallelrescan;

	PG_RETURN_POINTER(fc_amroutine);
}

/*
 *	btbuildempty() -- 在初始化分支中构建一个空的 B 树索引
 */
void btbuildempty(Relation fc_index)
{
	Page		fc_metapage;

	/* 构造元数据页。 */
	fc_metapage = (Page) palloc(BLCKSZ);
	_bt_initmetapage(fc_metapage, P_NONE, 0, _bt_allequalimage(fc_index, false));

	/*
	 * 写入页面并记录。看似立即同步应该足以保证文件
	 * 在磁盘上存在，但恢复本身可能会在重放时将其删除，例如
	 * XLOG_DBASE_CREATE* 或 XLOG_TBLSPC_CREATE记录。因此，
	 * 即使wal_level=minimal，我们也需要这样做。
	 */
	PageSetChecksumInplace(fc_metapage, BTREE_METAPAGE);
	smgrwrite(RelationGetSmgr(fc_index), INIT_FORKNUM, BTREE_METAPAGE,
			  (char *) fc_metapage, true);
	log_newpage(&RelationGetSmgr(fc_index)->smgr_rnode.node, INIT_FORKNUM,
				BTREE_METAPAGE, fc_metapage, true);

	/*
	 * 即使我们记录了页面，立即同步也是必要的，因为
	 * 写入没有经过shared_buffers，因此一个并发
	 * 检查点可能已经将重做指针移过我们的xlog记录。
	 */
	smgrimmedsync(RelationGetSmgr(fc_index), INIT_FORKNUM);
}

/*
 *	btinsert() -- 将索引元组插入到 B 树中。
 *
 *		递归下降树，找到适合我们新
 *		元组的位置，并将其放置在那里。
 */
bool btinsert(Relation fc_rel, Datum *fc_values, bool *fc_isnull,
		 ItemPointer fc_ht_ctid, Relation fc_heapRel,
		 IndexUniqueCheck fc_checkUnique,
		 bool fc_indexUnchanged,
		 IndexInfo *fc_indexInfo)
{
	bool		fc_result;
	IndexTuple	fc_itup;

	/* 生成索引元组 */
	fc_itup = index_form_tuple(RelationGetDescr(fc_rel), fc_values, fc_isnull);
	fc_itup->t_tid = *fc_ht_ctid;

	fc_result = _bt_doinsert(fc_rel, fc_itup, fc_checkUnique, fc_indexUnchanged, fc_heapRel);

	pfree(fc_itup);

	return fc_result;
}

/*
 *	btgettuple() -- 获取扫描中的下一个元组。
 */
bool btgettuple(IndexScanDesc fc_scan, ScanDirection fc_dir)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	bool		fc_res;

	/* B 树索引从不丢失 */
	fc_scan->xs_recheck = false;

	/*
	 * 如果我们有任何数组键，则在首次调用
	 * 扫描时初始化它们。我们不能在 btrescan 中这样做，因为
	 * 此时我们不知道扫描方向。
	 */
	if (fc_so->numArrayKeys && !BTScanPosIsValid(fc_so->currPos))
	{
		/* 如果我们有无法满足的数组键，则返回 */
		if (fc_so->numArrayKeys < 0)
			return false;

		_bt_start_array_keys(fc_scan, fc_dir);
	}

	/* 此循环处理前进到下一个数组元素（如果有） */
	do
	{
		/*
		 * 如果我们已经初始化了此扫描，可以直接在
		 * 适当的方向上推进。如果我们还没有这样做，
		 * 则调用 _bt_first() 获取扫描中的第一个项目。
		 */
		if (!BTScanPosIsValid(fc_so->currPos))
			fc_res = _bt_first(fc_scan, fc_dir);
		else
		{
			/*
			 * 检查是否应该终止之前获取的元组。
			 */
			if (fc_scan->kill_prior_tuple)
			{
				/*
				 * 是的，记住它以备后用。（我们将在离开索引页面之前一次性处理所有这样的元组。）对numKilled溢出的测试不仅仅是出于偏执：如果调用者在索引扫描中改变方向，则同一项可能会被多次输入。试图优化这一点没有意义，所以我们不检测它，而是简单地忘掉任何多余的条目。
				 */
				if (fc_so->killedItems == NULL)
					fc_so->killedItems = (int *)
						palloc(MaxTIDsPerBTreePage * sizeof(int));
				if (fc_so->numKilled < MaxTIDsPerBTreePage)
					fc_so->killedItems[fc_so->numKilled++] = fc_so->currPos.itemIndex;
			}

			/*
			 * 现在继续扫描。
			 */
			fc_res = _bt_next(fc_scan, fc_dir);
		}

		/* 如果我们有一个元组，返回它... */
		if (fc_res)
			break;
		/* ...否则看看我们是否还有更多的数组键要处理 */
	} while (fc_so->numArrayKeys && _bt_advance_array_keys(fc_scan, fc_dir));

	return fc_res;
}

/*
 * btgetbitmap() -- 获取所有匹配的元组，并将其添加到位图中
 */
int64 btgetbitmap(IndexScanDesc fc_scan, TIDBitmap *fc_tbm)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	int64		fc_ntids = 0;
	ItemPointer fc_heapTid;

	/*
	 * 如果我们有任何数组键，初始化它们。
	 */
	if (fc_so->numArrayKeys)
	{
		/* 如果我们有无法满足的数组键，则返回 */
		if (fc_so->numArrayKeys < 0)
			return fc_ntids;

		_bt_start_array_keys(fc_scan, ForwardScanDirection);
	}

	/* 此循环处理前进到下一个数组元素（如果有） */
	do
	{
		/* 获取第一页和元组 */
		if (_bt_first(fc_scan, ForwardScanDirection))
		{
			/* 保存元组 ID，并继续扫描 */
			fc_heapTid = &fc_scan->xs_heaptid;
			tbm_add_tuples(fc_tbm, fc_heapTid, 1, false);
			fc_ntids++;

			for (;;)
			{
				/*
				 * 在页面内推进到下一个元组。这与_bt_next()中的简单情况相同。
				 */
				if (++fc_so->currPos.itemIndex > fc_so->currPos.lastItem)
				{
					/* 让_bt_next来完成繁重的工作 */
					if (!_bt_next(fc_scan, ForwardScanDirection))
						break;
				}

				/* 保存元组 ID，并继续扫描 */
				fc_heapTid = &fc_so->currPos.items[fc_so->currPos.itemIndex].heapTid;
				tbm_add_tuples(fc_tbm, fc_heapTid, 1, false);
				fc_ntids++;
			}
		}
		/* 现在看看我们是否还有更多的数组键要处理 */
	} while (fc_so->numArrayKeys && _bt_advance_array_keys(fc_scan, ForwardScanDirection));

	return fc_ntids;
}

/*
 *	btbeginscan() -- 在btree索引上开始扫描
 */
IndexScanDesc btbeginscan(Relation fc_rel, int fc_nkeys, int fc_norderbys)
{
	IndexScanDesc fc_scan;
	BTScanOpaque fc_so;

	/* 不允许使用 order by 操作符 */
	Assert(fc_norderbys == 0);

	/* 获取扫描 */
	fc_scan = RelationGetIndexScan(fc_rel, fc_nkeys, fc_norderbys);

	/* 分配私有工作空间 */
	fc_so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
	BTScanPosInvalidate(fc_so->currPos);
	BTScanPosInvalidate(fc_so->markPos);
	if (fc_scan->numberOfKeys > 0)
		fc_so->keyData = (ScanKey) palloc(fc_scan->numberOfKeys * sizeof(ScanKeyData));
	else
		fc_so->keyData = NULL;

	fc_so->arrayKeyData = NULL;	/* 暂时假设没有数组键 */
	fc_so->arraysStarted = false;
	fc_so->numArrayKeys = 0;
	fc_so->arrayKeys = NULL;
	fc_so->arrayContext = NULL;

	fc_so->killedItems = NULL;		/* 等待需要时再使用 */
	fc_so->numKilled = 0;

	/*
	 * 我们还不知道扫描是否会是只索引的，因此在btrescan之前不会分配元组工作区数组。然而，无论我们是否需要它，我们都设置scan->xs_itupdesc，因为这开销非常小。
	 */
	fc_so->currTuples = fc_so->markTuples = NULL;

	fc_scan->xs_itupdesc = RelationGetDescr(fc_rel);

	fc_scan->opaque = fc_so;

	return fc_scan;
}

/*
 *	btrescan() -- 重新扫描一个索引关系
 */
void btrescan(IndexScanDesc fc_scan, ScanKey fc_scankey, int fc_nscankeys,
		 ScanKey fc_orderbys, int fc_norderbys)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;

	/* 我们没有持有任何读取锁，但必须放弃插销 */
	if (BTScanPosIsValid(fc_so->currPos))
	{
		/* 在离开当前页面之前，处理任何已删除的项 */
		if (fc_so->numKilled > 0)
			_bt_killitems(fc_scan);
		BTScanPosUnpinIfPinned(fc_so->currPos);
		BTScanPosInvalidate(fc_so->currPos);
	}

	fc_so->markItemIndex = -1;
	fc_so->arrayKeyCount = 0;
	BTScanPosUnpinIfPinned(fc_so->markPos);
	BTScanPosInvalidate(fc_so->markPos);

	/*
	 * 如果需要索引仅扫描，且之前的重新扫描调用中尚未完成，则分配元组工作区数组。为了节省palloc开销，这两个工作区作为一个palloc块分配；只有这个函数和btendscan知道这一点。
	 *
	 * 注：这个数据结构也使得从“name”列返回数据是安全的，即使btree name_ops使用了基础存储数据类型cstring。风险在于“name”应该填充到NAMEDATALEN，但实际的索引元组可能更短。然而，由于我们只返回currTuples数组中元组的数据，提取NAMEDATALEN字节最多只会从markTuples数组中提取一些数据——无法导致SIGSEGV的内存溢出。是的，这很丑，但总比在其他地方为name_ops添加特殊处理要好。
	 */
	if (fc_scan->xs_want_itup && fc_so->currTuples == NULL)
	{
		fc_so->currTuples = (char *) palloc(BLCKSZ * 2);
		fc_so->markTuples = fc_so->currTuples + BLCKSZ;
	}

	/*
	 * 重置扫描键
	 */
	if (fc_scankey && fc_scan->numberOfKeys > 0)
		memmove(fc_scan->keyData,
				fc_scankey,
				fc_scan->numberOfKeys * sizeof(ScanKeyData));
	fc_so->numberOfKeys = 0;		/* 直到_bt_preprocess_keys设置它 */

	/* 如果有任何键是SK_SEARCHARRAY类型，请设置数组键信息 */
	_bt_preprocess_array_keys(fc_scan);
}

/*
 *	btendscan() -- 关闭扫描
 */
void btendscan(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;

	/* 我们没有持有任何读取锁，但必须放弃插销 */
	if (BTScanPosIsValid(fc_so->currPos))
	{
		/* 在离开当前页面之前，处理任何已删除的项 */
		if (fc_so->numKilled > 0)
			_bt_killitems(fc_scan);
		BTScanPosUnpinIfPinned(fc_so->currPos);
	}

	fc_so->markItemIndex = -1;
	BTScanPosUnpinIfPinned(fc_so->markPos);

	/* 无需使位置失效，RAM 即将被释放。 */

	/* 释放存储 */
	if (fc_so->keyData != NULL)
		pfree(fc_so->keyData);
	/* so->arrayKeyData 和 so->arrayKeys 在 arrayContext 中 */
	if (fc_so->arrayContext != NULL)
		MemoryContextDelete(fc_so->arrayContext);
	if (fc_so->killedItems != NULL)
		pfree(fc_so->killedItems);
	if (fc_so->currTuples != NULL)
		pfree(fc_so->currTuples);
	/* so->markTuples 不应被 pfree，见 btrescan */
	pfree(fc_so);
}

/*
 *	btmarkpos() -- 保存当前扫描位置
 */
void btmarkpos(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;

	/* 可能有一个旧的标记带有 pin（但没有锁）。 */
	BTScanPosUnpinIfPinned(fc_so->markPos);

	/*
	 * 仅记录当前 itemIndex。如果我们稍后在释放标记位置之前
	 * 步进到下一页，_bt_steppage 会在 markPos 中完整复制
	 * currPos 结构。如果（如常发生的那样）标记在离开页面之前被移动，
	 * 我们就不必执行那么多工作。
	 */
	if (BTScanPosIsValid(fc_so->currPos))
		fc_so->markItemIndex = fc_so->currPos.itemIndex;
	else
	{
		BTScanPosInvalidate(fc_so->markPos);
		fc_so->markItemIndex = -1;
	}

	/* 还记录任何数组键的当前位置信息 */
	if (fc_so->numArrayKeys)
		_bt_mark_array_keys(fc_scan);
}

/*
 *	btrestrpos() -- 恢复到上一个保存的位置
 */
void btrestrpos(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;

	/* 恢复任何数组键的标记位置 */
	if (fc_so->numArrayKeys)
		_bt_restore_array_keys(fc_scan);

	if (fc_so->markItemIndex >= 0)
	{
		/*
		 * 自上次标记以来，扫描从未移动到新页。仅需
		 * 恢复 itemIndex。
		 *
		 * 注意：在这种情况下，我们不能依赖 so->markPos 中的任何
		 * 信息是准确的。
		 */
		fc_so->currPos.itemIndex = fc_so->markItemIndex;
	}
	else
	{
		/*
		 * 在上一个标记或恢复后，扫描已移动到新页，而我们现在
		 * 正在恢复到标记页。我们没有持有任何读取锁，
		 * 但如果我们仍在持有当前位的 pin，我们必须放弃它。
		 */
		if (BTScanPosIsValid(fc_so->currPos))
		{
			/* 在离开当前页面之前，处理任何已删除的项 */
			if (fc_so->numKilled > 0)
				_bt_killitems(fc_scan);
			BTScanPosUnpinIfPinned(fc_so->currPos);
		}

		if (BTScanPosIsValid(fc_so->markPos))
		{
			/* 为分配给当前缓冲区而提升标记缓冲区的 pin */
			if (BTScanPosIsPinned(fc_so->markPos))
				IncrBufferRefCount(fc_so->markPos.buf);
			memcpy(&fc_so->currPos, &fc_so->markPos,
				   offsetof(BTScanPosData, items[1]) +
				   fc_so->markPos.lastItem * sizeof(BTScanPosItem));
			if (fc_so->currTuples)
				memcpy(fc_so->currTuples, fc_so->markTuples,
					   fc_so->markPos.nextTupleOffset);
		}
		else
			BTScanPosInvalidate(fc_so->currPos);
	}
}

/*
 * btestimateparallelscan -- 估算 BTParallelScanDescData 的存储
 */
Size btestimateparallelscan(void)
{
	return sizeof(BTParallelScanDescData);
}

/*
 * btinitparallelscan -- 初始化 BTParallelScanDesc 以进行并行 btree 扫描
 */
void btinitparallelscan(void *fc_target)
{
	BTParallelScanDesc fc_bt_target = (BTParallelScanDesc) fc_target;

	SpinLockInit(&fc_bt_target->btps_mutex);
	fc_bt_target->btps_scanPage = InvalidBlockNumber;
	fc_bt_target->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
	fc_bt_target->btps_arrayKeyCount = 0;
	ConditionVariableInit(&fc_bt_target->btps_cv);
}

/*
 *	btparallelrescan() -- 重置并行扫描
 */
void btparallelrescan(IndexScanDesc fc_scan)
{
	BTParallelScanDesc fc_btscan;
	ParallelIndexScanDesc fc_parallel_scan = fc_scan->parallel_scan;

	Assert(fc_parallel_scan);

	fc_btscan = (BTParallelScanDesc) OffsetToPointer((void *) fc_parallel_scan,
												  fc_parallel_scan->ps_offset);

	/*
	 * 理论上，我们在这里不需要获取自旋锁，因为此时不应
	 * 还有其他工作者在运行，但为了保持一致性，我们这样做。
	 */
	SpinLockAcquire(&fc_btscan->btps_mutex);
	fc_btscan->btps_scanPage = InvalidBlockNumber;
	fc_btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
	fc_btscan->btps_arrayKeyCount = 0;
	SpinLockRelease(&fc_btscan->btps_mutex);
}

/*
 * _bt_parallel_seize() -- 开始将扫描推进到新页面的过程。
 * 其他扫描必须等到我们调用 _bt_parallel_release() 或
 * _bt_parallel_done()。
 *
 * 返回值为 true 表示我们已成功获取扫描控制，false 表示未成功。
 * 后者的情况发生在当前 scankeys 的页面已用尽时。
 *
 * 如果返回值为 true，*pageno 返回扫描的下一页或当前页
 * （取决于扫描方向）。无效的块编号意味着扫描尚未开始，
 * P_NONE 表示已到达末尾。参与过程第一次到达最后一页时，
 * 将返回 true 并将 *pageno 设置为 P_NONE；之后，再尝试获取
 * 扫描将返回 false。
 *
 * 调用者在返回值为 false 时应忽略 pageno 的值。
 */
bool _bt_parallel_seize(IndexScanDesc fc_scan, BlockNumber *fc_pageno)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	BTPS_State	fc_pageStatus;
	bool		fc_exit_loop = false;
	bool		fc_status = true;
	ParallelIndexScanDesc fc_parallel_scan = fc_scan->parallel_scan;
	BTParallelScanDesc fc_btscan;

	*fc_pageno = P_NONE;

	fc_btscan = (BTParallelScanDesc) OffsetToPointer((void *) fc_parallel_scan,
												  fc_parallel_scan->ps_offset);

	while (1)
	{
		SpinLockAcquire(&fc_btscan->btps_mutex);
		fc_pageStatus = fc_btscan->btps_pageStatus;

		if (fc_so->arrayKeyCount < fc_btscan->btps_arrayKeyCount)
		{
			/* 并行扫描已成功推进到新的 scankeys 集合。 */
			fc_status = false;
		}
		else if (fc_pageStatus == BTPARALLEL_DONE)
		{
			/*
			 * 我们已经完成了这一组 scankeys。这可能是结束，
			 * 或者可能还有更多的集合需要尝试。
			 */
			fc_status = false;
		}
		else if (fc_pageStatus != BTPARALLEL_ADVANCING)
		{
			/*
			 * 我们已成功控制扫描，以便将其推进到新页面！
			 */
			fc_btscan->btps_pageStatus = BTPARALLEL_ADVANCING;
			*fc_pageno = fc_btscan->btps_scanPage;
			fc_exit_loop = true;
		}
		SpinLockRelease(&fc_btscan->btps_mutex);
		if (fc_exit_loop || !fc_status)
			break;
		ConditionVariableSleep(&fc_btscan->btps_cv, WAIT_EVENT_BTREE_PAGE);
	}
	ConditionVariableCancelSleep();

	return fc_status;
}

/*
 * _bt_parallel_release() -- 完成将扫描推进到新页面的过程。我们现在有新的值 btps_scanPage；其他后端现在可以开始推进扫描。
 */
void _bt_parallel_release(IndexScanDesc fc_scan, BlockNumber fc_scan_page)
{
	ParallelIndexScanDesc fc_parallel_scan = fc_scan->parallel_scan;
	BTParallelScanDesc fc_btscan;

	fc_btscan = (BTParallelScanDesc) OffsetToPointer((void *) fc_parallel_scan,
												  fc_parallel_scan->ps_offset);

	SpinLockAcquire(&fc_btscan->btps_mutex);
	fc_btscan->btps_scanPage = fc_scan_page;
	fc_btscan->btps_pageStatus = BTPARALLEL_IDLE;
	SpinLockRelease(&fc_btscan->btps_mutex);
	ConditionVariableSignal(&fc_btscan->btps_cv);
}

/*
 * _bt_parallel_done() -- 标记并行扫描为完成。
 *
 * 当没有剩余页面可供扫描时，应调用此函数以通知其他工作线程。否则，他们可能会永远等待扫描推进到下一页。
 */
void _bt_parallel_done(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	ParallelIndexScanDesc fc_parallel_scan = fc_scan->parallel_scan;
	BTParallelScanDesc fc_btscan;
	bool		fc_status_changed = false;

	/* 对于非并行扫描不做任何操作 */
	if (fc_parallel_scan == NULL)
		return;

	fc_btscan = (BTParallelScanDesc) OffsetToPointer((void *) fc_parallel_scan,
												  fc_parallel_scan->ps_offset);

	/*
	 * 标记该组合扫描键的并行扫描已完成，除非其他进程已经这样做。另见 _bt_advance_array_keys。
	 */
	SpinLockAcquire(&fc_btscan->btps_mutex);
	if (fc_so->arrayKeyCount >= fc_btscan->btps_arrayKeyCount &&
		fc_btscan->btps_pageStatus != BTPARALLEL_DONE)
	{
		fc_btscan->btps_pageStatus = BTPARALLEL_DONE;
		fc_status_changed = true;
	}
	SpinLockRelease(&fc_btscan->btps_mutex);

	/* 唤醒与此并行扫描相关的所有工作线程 */
	if (fc_status_changed)
		ConditionVariableBroadcast(&fc_btscan->btps_cv);
}

/*
 * _bt_parallel_advance_array_keys() -- 推进数组键的并行扫描。
 *
 * 更新本地以及并行扫描所处理的数组键计数。
 */
void _bt_parallel_advance_array_keys(IndexScanDesc fc_scan)
{
	BTScanOpaque fc_so = (BTScanOpaque) fc_scan->opaque;
	ParallelIndexScanDesc fc_parallel_scan = fc_scan->parallel_scan;
	BTParallelScanDesc fc_btscan;

	fc_btscan = (BTParallelScanDesc) OffsetToPointer((void *) fc_parallel_scan,
												  fc_parallel_scan->ps_offset);

	fc_so->arrayKeyCount++;
	SpinLockAcquire(&fc_btscan->btps_mutex);
	if (fc_btscan->btps_pageStatus == BTPARALLEL_DONE)
	{
		fc_btscan->btps_scanPage = InvalidBlockNumber;
		fc_btscan->btps_pageStatus = BTPARALLEL_NOT_INITIALIZED;
		fc_btscan->btps_arrayKeyCount++;
	}
	SpinLockRelease(&fc_btscan->btps_mutex);
}

/*
 * 批量删除所有指向一组堆元组的索引条目。
 * 目标元组集通过一个回调例程指定，该例程指示
 * 是否任何给定的堆元组（通过 ItemPointer 标识）正在被删除。
 *
 * 结果：一个 palloc 的结构，包含 VACUUM 显示的统计信息。
 */
IndexBulkDeleteResult * btbulkdelete(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats,
			 IndexBulkDeleteCallback fc_callback, void *fc_callback_state)
{
	Relation	fc_rel = fc_info->index;
	BTCycleId	fc_cycleid;

	/* 如果第一次遍历则分配统计信息，否则重用现有结构 */
	if (fc_stats == NULL)
		fc_stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));

	/* 建立用于此扫描的 VACUUM 周期 ID */
	/* ENSURE 相关的内容确保我们在失败时清理共享内存 */
	PG_ENSURE_ERROR_CLEANUP(_bt_end_vacuum_callback, PointerGetDatum(fc_rel));
	{
		fc_cycleid = _bt_start_vacuum(fc_rel);

		fc_btvacuumscan(fc_info, fc_stats, fc_callback, fc_callback_state, fc_cycleid);
	}
	PG_END_ENSURE_ERROR_CLEANUP(_bt_end_vacuum_callback, PointerGetDatum(fc_rel));
	_bt_end_vacuum(fc_rel);

	return fc_stats;
}

/*
 * 后VACUUM清理。
 *
 * 结果：一个palloc'd结构包含VACUUM显示的统计信息。
 */
IndexBulkDeleteResult * btvacuumcleanup(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats)
{
	BlockNumber fc_num_delpages;

	/* 在ANALYZE ONLY模式下无操作 */
	if (fc_info->analyze_only)
		return fc_stats;

	/*
	 * 如果调用了 btbulkdelete，则不需要执行任何操作（我们只需通过调用下面的 _bt_set_cleanup_info() 维护在 _bt_vacuum_needs_cleanup() 中使用的信息）。
	 *
	 * 如果没有调用 btbulkdelete，那么我们需要做出选择：我们必须决定现在是否需要调用 btvacuumscan()（即，当前的 VACUUM 操作是否可以完全避免对索引的物理扫描）。对 _bt_vacuum_needs_cleanup() 的调用现在为我们决定了这一点。
	 */
	if (fc_stats == NULL)
	{
		/* 检查 VACUUM 操作是否可以完全避免 btvacuumscan() 调用 */
		if (!_bt_vacuum_needs_cleanup(fc_info->index))
			return NULL;

		/*
		 * 由于我们不会实际删除任何叶子项，因此不需要在此进行所有的 VACUUM 周期 ID 推送。
		 *
		 * 发布列表元组是清理专用扫描的不准确来源。btvacuumscan() 将假设每页的索引元组数量可以用作 num_index_tuples，即使 num_index_tuples 应该表示索引中的 TIDs 数量。这种天真的做法可能会显著低估索引中的元组数量。
		 *
		 * 我们通过在清理专用情况下将 num_index_tuples 设定为估计值来处理这个问题。
		 */
		fc_stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
		fc_btvacuumscan(fc_info, fc_stats, NULL, NULL, 0);
		fc_stats->estimated_count = true;
	}

	/*
	 * 在元页面中维护 num_delpages 值，以便用于 _bt_vacuum_needs_cleanup()。
	 *
	 * num_delpages 是当前索引中已删除页面的数量，这些页面尚不安全
	 * 置于 FSM 中以进行回收。只有当 _bt_pagedel() 在我们调用
	 * btvacuumscan() 时实际删除了页面，num_delpages 才大于 0。
	 * 即便如此，_bt_pendingfsm_finalize() 必须在短时间前未能将
	 * 新删除的任何页面放入 FSM。（实际上，在某些极端情况下，
	 * 当前 VACUUM 的新删除页面在下一个 VACUUM 到来时甚至不会变得
	 * 安全。参见 nbtree/README。）
	 */
	Assert(fc_stats->pages_deleted >= fc_stats->pages_free);
	fc_num_delpages = fc_stats->pages_deleted - fc_stats->pages_free;
	_bt_set_cleanup_info(fc_info->index, fc_num_delpages);

	/*
	 * 我们可能会被并发页面拆分所欺骗，导致某些索引元组被重复计数，
	 * 因此不相信任何超过底层堆计数的总数……如果我们准确知道这一点。
	 * 否则这可能会使问题变得更糟。
	 */
	if (!fc_info->estimated_count)
	{
		if (fc_stats->num_index_tuples > fc_info->num_heap_tuples)
			fc_stats->num_index_tuples = fc_info->num_heap_tuples;
	}

	return fc_stats;
}

/*
 * btvacuumscan --- 为 VACUUM 目的扫描索引
 *
 * 这结合了查找可根据 VACUUM 回调删除的叶元组，查找可以
 * 被删除的空页面，以及查找可以回收的旧删除页面的功能。
 * btbulkdelete 和 btvacuumcleanup 都调用这个（后者仅在没有
 * 发生 btbulkdelete 调用且 _bt_vacuum_needs_cleanup 返回真时）。
 *
 * 调用者负责最初分配/清零一个 stats 结构，并在必要时获取
 * VACUUM 循环 ID。
 */
static void fc_btvacuumscan(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats,
			 IndexBulkDeleteCallback fc_callback, void *fc_callback_state,
			 BTCycleId fc_cycleid)
{
	Relation	fc_rel = fc_info->index;
	BTVacState	fc_vstate;
	BlockNumber fc_num_pages;
	BlockNumber fc_scanblkno;
	bool		fc_needLock;

	/*
	 * 现在重置跟踪整个索引信息的字段。这避免了在单个 VACUUM 命令
	 * 需要多次扫描索引的情况下出现双重计数。
	 *
	 * 在这里避免重置 tuples_removed 和 pages_newly_deleted 字段，
	 * 因为它们跟踪与 VACUUM 命令相关的信息，因此必须跨
	 * 每次调用 btvacuumscan() 而持续存在。
	 *
	 * （注意，pages_free 被视为整个索引的状态，而不是当前 VACUUM。
	 * 这是适当的，因为 RecordFreeIndexPage() 调用是幂等的，并且
	 * 在某些场景中会对相同的删除页面进行重复调用。对我们来说，
	 * 关键是跟踪 VACUUM 命令结束时索引中可回收页面的数量。）
	 */
	fc_stats->num_pages = 0;
	fc_stats->num_index_tuples = 0;
	fc_stats->pages_deleted = 0;
	fc_stats->pages_free = 0;

	/* 设置要传递给 btvacuumpage 的信息 */
	fc_vstate.info = fc_info;
	fc_vstate.stats = fc_stats;
	fc_vstate.callback = fc_callback;
	fc_vstate.callback_state = fc_callback_state;
	fc_vstate.cycleid = fc_cycleid;

	/* 创建一个临时内存上下文以运行 _bt_pagedel */
	fc_vstate.pagedelcontext = AllocSetContextCreate(CurrentMemoryContext,
												  "_bt_pagedel",
												  ALLOCSET_DEFAULT_SIZES);

	/* 初始化 _bt_pendingfsm_finalize 使用的 vstate 字段 */
	fc_vstate.bufsize = 0;
	fc_vstate.maxbufsize = 0;
	fc_vstate.pendingpages = NULL;
	fc_vstate.npendingpages = 0;
	/* 考虑应用 _bt_pendingfsm_finalize 优化 */
	_bt_pendingfsm_init(fc_rel, &fc_vstate, (fc_callback == NULL));

	/*
	 * 外层循环遍历所有索引页面（除元页面外），按物理顺序进行（我们希望内核能合作提供预读以提高速度）。访问所有叶子页面是至关重要的，包括在我们开始扫描后新增的页面，否则我们可能无法删除一些可删除的元组。因此，我们必须反复检查关系长度。在执行此操作时，必须获取关系扩展锁以避免竞争条件：如果其他人正在扩展关系，则会有一个窗口，在该窗口中，bufmgr/smgr 创建了一个新的全零页面，但它尚未被 _bt_getbuf() 写锁定。如果我们在这里能够扫描到这样的页面，我们将错误地假设它可以被回收。获取锁可足够同步事物以防止出现问题：要么 num_pages 不会包括新页面，要么 _bt_getbuf 已经在缓冲区上有写锁，并且在我们检查之前，它将完全初始化。此外，如果在我们查看之后立即添加了页面，我们也无需担心；页面拆分代码在添加右页面之前已经对左页面进行了写锁，因此我们必须已经处理了任何应移动到该页面的元组。
	 *
	 * 然而，我们可以跳过对新关系或临时关系的锁定，因为没有其他人会访问它们。
	 */
	fc_needLock = !RELATION_IS_LOCAL(fc_rel);

	fc_scanblkno = BTREE_METAPAGE + 1;
	for (;;)
	{
		/* 获取当前关系长度 */
		if (fc_needLock)
			LockRelationForExtension(fc_rel, ExclusiveLock);
		fc_num_pages = RelationGetNumberOfBlocks(fc_rel);
		if (fc_needLock)
			UnlockRelationForExtension(fc_rel, ExclusiveLock);

		if (fc_info->report_progress)
			pgstat_progress_update_param(PROGRESS_SCAN_BLOCKS_TOTAL,
										 fc_num_pages);

		/* 如果我们扫描了整个关系，则退出 */
		if (fc_scanblkno >= fc_num_pages)
			break;
		/* 遍历页面，然后循环回去重新检查长度 */
		for (; fc_scanblkno < fc_num_pages; fc_scanblkno++)
		{
			fc_btvacuumpage(&fc_vstate, fc_scanblkno);
			if (fc_info->report_progress)
				pgstat_progress_update_param(PROGRESS_SCAN_BLOCKS_DONE,
											 fc_scanblkno);
		}
	}

	/* 将统计信息 num_pages 字段设置为索引的最终大小 */
	fc_stats->num_pages = fc_num_pages;

	MemoryContextDelete(fc_vstate.pagedelcontext);

	/*
	 * 如果在索引扫描期间有任何对 _bt_pagedel() 的调用，则查看任何结果页面是否可以现在放入 FSM 中。当不安全时，我们必须将其留给将来的 VACUUM 操作。
	 *
	 * 最后，如果我们在 FSM 中放置了任何页面（刚刚或在扫描期间），强制更新上层 FSM 页面以确保搜索者能够找到它们。
	 */
	_bt_pendingfsm_finalize(fc_rel, &fc_vstate);
	if (fc_stats->pages_free > 0)
		IndexFreeSpaceMapVacuum(fc_rel);
}

/*
 * btvacuumpage --- VACUUM 一页
 *
 * 这处理了 btvacuumscan() 的单个页面。在某些情况下，我们必须回溯以重新检查和 VACUUM 在之前的调用期间的 scanblkno 的页面。这是我们处理页面拆分的方法（发生在我们获取 cycleid 之后），其右半部分页面恰好重新使用了我们可能在被回收之前处理过的块（即在页面拆分之前）。
 */
static void fc_btvacuumpage(BTVacState *fc_vstate, BlockNumber fc_scanblkno)
{
	IndexVacuumInfo *fc_info = fc_vstate->info;
	IndexBulkDeleteResult *fc_stats = fc_vstate->stats;
	IndexBulkDeleteCallback fc_callback = fc_vstate->callback;
	void	   *fc_callback_state = fc_vstate->callback_state;
	Relation	fc_rel = fc_info->index;
	bool		fc_attempt_pagedel;
	BlockNumber fc_blkno,
				fc_backtrack_to;
	Buffer		fc_buf;
	Page		fc_page;
	BTPageOpaque fc_opaque;

	fc_blkno = fc_scanblkno;

backtrack:

	fc_attempt_pagedel = false;
	fc_backtrack_to = P_NONE;

	/* 在不持有任何缓冲区锁的情况下调用vacuum_delay_point */
	vacuum_delay_point();

	/*
	 * 我们不能在这里使用 _bt_getbuf()，因为它总是应用 _bt_checkpage()，而 _bt_checkpage() 会在全零页面上失败。我们想要回收全零页面，而不是失败。此外，我们想要使用非默认的缓冲访问策略。
	 */
	fc_buf = ReadBufferExtended(fc_rel, MAIN_FORKNUM, fc_blkno, RBM_NORMAL,
							 fc_info->strategy);
	_bt_lockbuf(fc_rel, fc_buf, BT_READ);
	fc_page = BufferGetPage(fc_buf);
	fc_opaque = NULL;
	if (!PageIsNew(fc_page))
	{
		_bt_checkpage(fc_rel, fc_buf);
		fc_opaque = BTPageGetOpaque(fc_page);
	}

	Assert(fc_blkno <= fc_scanblkno);
	if (fc_blkno != fc_scanblkno)
	{
		/* 我们正在回溯。  
 *   
 * 我们跟随一个右链接到一个兄弟叶子页面（一个恰好来自于一个在scanblkno之前的块的页面）。我们唯一想处理的情况是具有当前真空循环ID的活动叶子页面。  
 *   
 * 这个页面最好处于与我们期望的一致的状态。检查可能暗示损坏的条件。它不能是半死状态，因为只有中断的VACUUM过程才能将页面留在该状态，因此当页面是scanblkno页面时，我们绝对会处理它（半死页面总是被_bt_pagedel()标记为完全删除，除非有损坏）。  
 */  
		if (!fc_opaque || !P_ISLEAF(fc_opaque) || P_ISHALFDEAD(fc_opaque))
		{
			Assert(false);
			ereport(LOG,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg_internal("right sibling %u of scanblkno %u unexpectedly in an inconsistent state in index \"%s\"",
									 fc_blkno, fc_scanblkno, RelationGetRelationName(fc_rel))));
			_bt_relbuf(fc_rel, fc_buf);
			return;
		}

		/*  
 * 我们可能已经在之前的调用中处理过这个页面，当时页面是scanblkno。这种情况发生在叶子页面拆分发生在扫描开始之后，但在右兄弟页面成为scanblkno之前。  
 *   
 * 页面也可能已被当前的btvacuumpage()调用删除，因为_bt_pagedel()有时在处理过程中删除scanblkno的右兄弟页面（在我们决定回溯到哪里后进行）。我们现在不需要第二次将这个页面作为已删除页面进行处理（事实上，第二次将其计入批量删除统计会是错误的）。  
 */  
		if (fc_opaque->btpo_cycleid != fc_vstate->cycleid || P_ISDELETED(fc_opaque))
		{
			/* 当前scanblkno（及所有较低拆分页面）已完成 */  
			_bt_relbuf(fc_rel, fc_buf);
			return;
		}
	}

	if (!fc_opaque || BTPageIsRecyclable(fc_page))
	{
		/* 可以回收这个页面（可能是叶子或内部） */  
		RecordFreeIndexPage(fc_rel, fc_blkno);
		fc_stats->pages_deleted++;
		fc_stats->pages_free++;
	}
	else if (P_ISDELETED(fc_opaque))
	{
		/* 已经删除的页面（可能是叶子或内部）。尚不能回收。 */  
		fc_stats->pages_deleted++;
	}
	else if (P_ISHALFDEAD(fc_opaque))
	{
		/* 半死的叶子页面（来自中断的VACUUM）-- 完成删除 */  
		fc_attempt_pagedel = true;

		/*  
 * _bt_pagedel()在所有情况下（除非有损坏）都会增加both pages_newly_deleted 和 pages_deleted的统计数据  
 */  
	}
	else if (P_ISLEAF(fc_opaque))
	{
		OffsetNumber fc_deletable[MaxIndexTuplesPerPage];
		int			fc_ndeletable;
		BTVacuumPosting fc_updatable[MaxIndexTuplesPerPage];
		int			fc_nupdatable;
		OffsetNumber fc_offnum,
					fc_minoff,
					fc_maxoff;
		int			fc_nhtidsdead,
					fc_nhtidslive;

		/*  
 * 用初始读取锁换取这个页面的完全清理锁。我们必须在真空扫描过程中对每个叶子页面获得这样的锁，无论它是否实际包含任何可删除的元组——请参见nbtree/README。  
 */  
		_bt_upgradelockbufcleanup(fc_rel, fc_buf);

		/*  
 * 检查我们是否需要回溯到更早的页面。我们所关心的是自从我们开始真空扫描以来发生的页面拆分。如果拆分将拆分的右半部分（即排序较高的元组）上的元组移动到我们已经经过的块中，那么我们可能已经错过了这些元组。我们现在需要回溯。（必须在可能清除btpo_cycleid或删除scanblkno页面之前进行此操作！）  
 */  
		if (fc_vstate->cycleid != 0 &&
			fc_opaque->btpo_cycleid == fc_vstate->cycleid &&
			!(fc_opaque->btpo_flags & BTP_SPLIT_END) &&
			!P_RIGHTMOST(fc_opaque) &&
			fc_opaque->btpo_next < fc_scanblkno)
			fc_backtrack_to = fc_opaque->btpo_next;

		fc_ndeletable = 0;
		fc_nupdatable = 0;
		fc_minoff = P_FIRSTDATAKEY(fc_opaque);
		fc_maxoff = PageGetMaxOffsetNumber(fc_page);
		fc_nhtidsdead = 0;
		fc_nhtidslive = 0;
		if (fc_callback)
		{
			/* btbulkdelete回调告诉我们要删除（或更新）什么 */  
			for (fc_offnum = fc_minoff;
				 fc_offnum <= fc_maxoff;
				 fc_offnum = OffsetNumberNext(fc_offnum))
			{
				IndexTuple	fc_itup;

				fc_itup = (IndexTuple) PageGetItem(fc_page,
												PageGetItemId(fc_page, fc_offnum));

				Assert(!BTreeTupleIsPivot(fc_itup));
				if (!BTreeTupleIsPosting(fc_itup))
				{
					/* 常规元组，标准表TID表示 */  
					if (fc_callback(&fc_itup->t_tid, fc_callback_state))
					{
						fc_deletable[fc_ndeletable++] = fc_offnum;
						fc_nhtidsdead++;
					}
					else
						fc_nhtidslive++;
				}
				else
				{
					BTVacuumPosting fc_vacposting;
					int			fc_nremaining;

					/* 帖子列表元组 */  
					fc_vacposting = fc_btreevacuumposting(fc_vstate, fc_itup, fc_offnum,
													&fc_nremaining);
					if (fc_vacposting == NULL)
					{
						/*  
 * 帖子元组中的所有表TIDs仍然存在，因此不需要删除或更新  
 */
						Assert(fc_nremaining == BTreeTupleGetNPosting(fc_itup));
					}
					else if (fc_nremaining > 0)
					{

						/*
						 * 将关于发布列表元组的元数据存储在
						 * 可更新数组中，供整个页面使用。现有元组
						 * 将在稍后的调用中进行更新
						 * _bt_delitems_vacuum()。
						 */
						Assert(fc_nremaining < BTreeTupleGetNPosting(fc_itup));
						fc_updatable[fc_nupdatable++] = fc_vacposting;
						fc_nhtidsdead += BTreeTupleGetNPosting(fc_itup) - fc_nremaining;
					}
					else
					{
						/*
						 * 发布列表中的所有表 TID 必须被删除。
						 * 我们将完全删除索引元组
						 * （无需更新）。
						 */
						Assert(fc_nremaining == 0);
						fc_deletable[fc_ndeletable++] = fc_offnum;
						fc_nhtidsdead += BTreeTupleGetNPosting(fc_itup);
						pfree(fc_vacposting);
					}

					fc_nhtidslive += fc_nremaining;
				}
			}
		}

		/*
		 * 应用任何需要的删除或更新。我们每页只发出一次
		 * _bt_delitems_vacuum() 调用，以最小化 WAL 流量。
		 */
		if (fc_ndeletable > 0 || fc_nupdatable > 0)
		{
			Assert(fc_nhtidsdead >= fc_ndeletable + fc_nupdatable);
			_bt_delitems_vacuum(fc_rel, fc_buf, fc_deletable, fc_ndeletable, fc_updatable,
								fc_nupdatable);

			fc_stats->tuples_removed += fc_nhtidsdead;
			/* 必须重新计算 maxoff */
			fc_maxoff = PageGetMaxOffsetNumber(fc_page);

			/* 这里不能泄漏内存 */
			for (int fc_i = 0; fc_i < fc_nupdatable; fc_i++)
				pfree(fc_updatable[fc_i]);
		}
		else
		{
			/*
			 * 如果在此清理周期中叶页面已被拆分，
			 * 即使没有任何删除操作，清除 btpo_cycleid 似乎也是值得
			 * 的。（如果有，则由 _bt_delitems_vacuum
			 * 处理。）这确保我们不会再次处理该页面。
			 *
			 * 我们将此视为提示位更新，因为没有必要
			 * WAL 记录它。
			 */
			Assert(fc_nhtidsdead == 0);
			if (fc_vstate->cycleid != 0 &&
				fc_opaque->btpo_cycleid == fc_vstate->cycleid)
			{
				fc_opaque->btpo_cycleid = 0;
				MarkBufferDirtyHint(fc_buf, true);
			}
		}

		/*
		 * 如果叶页面现在是空的，尝试删除它；否则计数
		 * 活跃元组（发布列表中的活跃表 TID 作为
		 * 单独的活跃元组进行计数）。不过，在回溯时我们不删除，
		 * 因为这将需要让 _bt_pagedel() 理解回溯
		 * （将复杂性增加到处理此问题上似乎不值得）。
		 *
		 * 在仅进行清理的 btvacuumscan 调用中
		 * （即，当回调未设置时），我们不计数活跃 TID 的数量。
		 * 而是直接计数索引元组的数量。这样可以避免
		 * 直接检查每个页面上所有元组的开销。VACUUM 将
		 * 在仅清理的情况下将 num_index_tuples 视为估算，
		 * 因此在某些情况下大幅低估 num_index_tuples 
		 * 也无关紧要。
		 */
		if (fc_minoff > fc_maxoff)
			fc_attempt_pagedel = (fc_blkno == fc_scanblkno);
		else if (fc_callback)
			fc_stats->num_index_tuples += fc_nhtidslive;
		else
			fc_stats->num_index_tuples += fc_maxoff - fc_minoff + 1;

		Assert(!fc_attempt_pagedel || fc_nhtidslive == 0);
	}

	if (fc_attempt_pagedel)
	{
		MemoryContext fc_oldcontext;

		/* 在临时上下文中运行 pagedel 以避免内存泄漏 */
		MemoryContextReset(fc_vstate->pagedelcontext);
		fc_oldcontext = MemoryContextSwitchTo(fc_vstate->pagedelcontext);

		/*
		 * _bt_pagedel 代表我们维护批量删除统计信息；
		 * pages_newly_deleted 和 pages_deleted 很可能会在调用期间递增。
		 */
		Assert(fc_blkno == fc_scanblkno);
		_bt_pagedel(fc_rel, fc_buf, fc_vstate);

		MemoryContextSwitchTo(fc_oldcontext);
		/* pagedel 释放了缓冲区，因此我们不应该 */
	}
	else
		_bt_relbuf(fc_rel, fc_buf);

	if (fc_backtrack_to != P_NONE)
	{
		fc_blkno = fc_backtrack_to;
		goto backtrack;
	}
}

/*
 * btreevacuumposting --- 确定发布列表中仍然需要的 TID
 *
 * 返回描述如何构建没有 VACUUM 需要删除的 TID 的替换元组的元数据。
 * 在没有对调用者的发布列表元组进行更改的常见情况下，
 * 返回值为 NULL（我们将此处的内存分配作为优化）。
 *
 * 应该保留在发布列表元组中的 TID 数量通过
 * *nremaining 设置给调用者。
 */
static BTVacuumPosting
fc_btreevacuumposting(BTVacState *fc_vstate, IndexTuple fc_posting,
				   OffsetNumber fc_updatedoffset, int *fc_nremaining)
{
	int			fc_live = 0;
	int			fc_nitem = BTreeTupleGetNPosting(fc_posting);
	ItemPointer fc_items = BTreeTupleGetPosting(fc_posting);
	BTVacuumPosting fc_vacposting = NULL;

	for (int fc_i = 0; fc_i < fc_nitem; fc_i++)
	{
		if (!fc_vstate->callback(fc_items + fc_i, fc_vstate->callback_state))
		{
			/* 活跃表 TID */
			fc_live++;
		}
		else if (fc_vacposting == NULL)
		{
			/*
			 * 遇到的第一个死表 TID。
			 *
			 * 现在很清楚我们需要删除一个或多个死表
			 * TID，因此开始维护描述如何更新
			 * 现有发布列表元组的元数据。
			 */
			fc_vacposting = palloc(offsetof(BTVacuumPostingData, deletetids) +
								fc_nitem * sizeof(uint16));

			fc_vacposting->itup = fc_posting;
			fc_vacposting->updatedoffset = fc_updatedoffset;
			fc_vacposting->ndeletedtids = 0;
			fc_vacposting->deletetids[fc_vacposting->ndeletedtids++] = fc_i;
		}
		else
		{
			/* 第二个或后续死表 TID */
			fc_vacposting->deletetids[fc_vacposting->ndeletedtids++] = fc_i;
		}
	}

	*fc_nremaining = fc_live;
	return fc_vacposting;
}

/*
 *	btcanreturn() -- 检查 btree 索引是否支持仅索引扫描。
 *
 * btrees 始终支持，因此这很简单。
 */
bool btcanreturn(Relation fc_index, int fc_attno)
{
	return true;
}
